增加stickyheader;增加设定时间api

chengzhenyu 8 years ago
parent
commit
0391985300
21 changed files with 2411 additions and 2 deletions
  1. 6 0
      app/src/main/AndroidManifest.xml
  2. 6 0
      app/src/main/java/ai/pai/lensman/main/MainActivity.java
  3. 2 2
      app/src/main/java/ai/pai/lensman/main/SessionRecyclerAdapter.java
  4. 30 0
      app/src/main/java/ai/pai/lensman/upload/UploadActivity.java
  5. 7 0
      app/src/main/java/ai/pai/lensman/utils/BoxUrlContainer.java
  6. 3 0
      app/src/main/java/ai/pai/lensman/utils/UrlContainer.java
  7. 1 0
      app/src/main/res/layout/activity_main.xml
  8. 68 0
      app/src/main/res/layout/activity_upload.xml
  9. 2 0
      app/src/main/res/values/strings.xml
  10. 225 0
      views/src/main/java/com/android/views/stickylistheaders/AdapterWrapper.java
  11. 31 0
      views/src/main/java/com/android/views/stickylistheaders/CheckableWrapperView.java
  12. 147 0
      views/src/main/java/com/android/views/stickylistheaders/DistinctMultiHashMap.java
  13. 39 0
      views/src/main/java/com/android/views/stickylistheaders/DualHashMap.java
  14. 131 0
      views/src/main/java/com/android/views/stickylistheaders/ExpandableStickyListHeadersAdapter.java
  15. 126 0
      views/src/main/java/com/android/views/stickylistheaders/ExpandableStickyListHeadersListView.java
  16. 32 0
      views/src/main/java/com/android/views/stickylistheaders/SectionIndexerAdapterWrapper.java
  17. 38 0
      views/src/main/java/com/android/views/stickylistheaders/StickyListHeadersAdapter.java
  18. 1131 0
      views/src/main/java/com/android/views/stickylistheaders/StickyListHeadersListView.java
  19. 156 0
      views/src/main/java/com/android/views/stickylistheaders/WrapperView.java
  20. 196 0
      views/src/main/java/com/android/views/stickylistheaders/WrapperViewList.java
  21. 34 0
      views/src/main/res/values/attrs.xml

+ 6 - 0
app/src/main/AndroidManifest.xml

@@ -128,6 +128,12 @@
128 128
             android:screenOrientation="portrait"/>
129 129
 
130 130
         <activity
131
+            android:name=".upload.UploadActivity"
132
+            android:configChanges="keyboardHidden|orientation|screenSize"
133
+            android:label="@string/app_name"
134
+            android:screenOrientation="portrait"/>
135
+
136
+        <activity
131 137
             android:name=".wxapi.WXEntryActivity"
132 138
             android:exported="true"
133 139
             android:launchMode="singleTop"

+ 6 - 0
app/src/main/java/ai/pai/lensman/main/MainActivity.java

@@ -22,6 +22,7 @@ import ai.pai.lensman.base.BaseActivity;
22 22
 import ai.pai.lensman.bean.SessionBean;
23 23
 import ai.pai.lensman.briefs.BriefsActivity;
24 24
 import ai.pai.lensman.session.SessionActivity;
25
+import ai.pai.lensman.upload.UploadActivity;
25 26
 import ai.pai.lensman.utils.UmengEvent;
26 27
 import butterknife.BindView;
27 28
 import butterknife.ButterKnife;
@@ -82,6 +83,11 @@ public class MainActivity extends BaseActivity implements MainContract.View {
82 83
         startActivity(new Intent(this, BriefsActivity.class));
83 84
     }
84 85
 
86
+    @OnClick(R.id.iv_upload_manage)
87
+    void jumpToUploadManage(){
88
+        startActivity(new Intent(this, UploadActivity.class));
89
+    }
90
+
85 91
     @OnClick(R.id.iv_add_session)
86 92
     void jumpToNewSession(){
87 93
         LogHelper.d(TAG,"jumpToNewSession");

+ 2 - 2
app/src/main/java/ai/pai/lensman/main/SessionRecyclerAdapter.java

@@ -81,7 +81,7 @@ public class SessionRecyclerAdapter extends RecyclerView.Adapter<SessionRecycler
81 81
         lp.width = width;
82 82
         lp.height = height;
83 83
         holder.photo.setLayoutParams(lp);
84
-        holder.sesseionSeq.setText(String.valueOf(item.sessionSeq));
84
+        holder.sessionSeq.setText(String.valueOf(item.sessionSeq));
85 85
         ArrayList<PhotoBean> photoList = item.sessionPhotos;
86 86
 
87 87
         if(photoList!=null && photoList.size()>0){
@@ -134,7 +134,7 @@ public class SessionRecyclerAdapter extends RecyclerView.Adapter<SessionRecycler
134 134
 
135 135
         @BindView(R.id.iv_session_item) ImageView photo;
136 136
 
137
-        @BindView(R.id.tv_session_seq) TextView sesseionSeq;
137
+        @BindView(R.id.tv_session_seq) TextView sessionSeq;
138 138
 
139 139
         @BindView(R.id.tv_session_upload_status) TextView uploadStatus;
140 140
 

+ 30 - 0
app/src/main/java/ai/pai/lensman/upload/UploadActivity.java

@@ -0,0 +1,30 @@
1
+package ai.pai.lensman.upload;
2
+
3
+import android.os.Bundle;
4
+
5
+import ai.pai.lensman.R;
6
+import ai.pai.lensman.base.BaseActivity;
7
+import butterknife.ButterKnife;
8
+import butterknife.OnClick;
9
+
10
+/**
11
+ * Created by chengzhenyu on 2017/4/4.
12
+ */
13
+
14
+public class UploadActivity extends BaseActivity{
15
+
16
+
17
+    @Override
18
+    protected void onCreate(Bundle savedInstanceState) {
19
+        super.onCreate(savedInstanceState);
20
+        setContentView(R.layout.activity_upload);
21
+        unbinder = ButterKnife.bind(this);
22
+    }
23
+
24
+    @OnClick(R.id.title_bar_back_layout)
25
+    void back(){
26
+        finish();
27
+    }
28
+
29
+
30
+}

+ 7 - 0
app/src/main/java/ai/pai/lensman/utils/BoxUrlContainer.java

@@ -25,6 +25,9 @@ public class BoxUrlContainer {
25 25
 
26 26
     public static String BOX_INFO_URL = BASE_URL+"box_info";
27 27
 
28
+    public static String SET_TIME_URL = BASE_URL+"set_time";
29
+
30
+    public static String BOX_TIME_URL = BASE_URL+"box_time";
28 31
 
29 32
     public static void resetIPHost(String ip) {
30 33
         BOX_IP = ip;
@@ -44,6 +47,10 @@ public class BoxUrlContainer {
44 47
         FETCH_ORIGIN_URL = BASE_URL +"fetch_origin";
45 48
 
46 49
         BOX_INFO_URL = BASE_URL+"box_info";
50
+
51
+       SET_TIME_URL = BASE_URL+"set_time";
52
+
53
+       BOX_TIME_URL = BASE_URL+"box_time";
47 54
     }
48 55
 
49 56
 

+ 3 - 0
app/src/main/java/ai/pai/lensman/utils/UrlContainer.java

@@ -68,4 +68,7 @@ public class UrlContainer {
68 68
     public static final String PLATFORM_PRICE_RULES_PAGE_URL = "http://pai.ai/page/price";
69 69
 
70 70
     public static final String PATCH_CONFIG_URL = HOST_URL+"op/patch";
71
+
72
+    public static final String SERVER_TIME_URL=HOST_URL+"s/server_time";
73
+
71 74
 }

+ 1 - 0
app/src/main/res/layout/activity_main.xml

@@ -36,6 +36,7 @@
36 36
                 android:textSize="@dimen/action_bar_title_medium_text_size" />
37 37
 
38 38
             <ImageView
39
+                android:id="@+id/iv_upload_manage"
39 40
                 android:layout_width="32dp"
40 41
                 android:layout_height="32dp"
41 42
                 android:padding="4dp"

+ 68 - 0
app/src/main/res/layout/activity_upload.xml

@@ -0,0 +1,68 @@
1
+<?xml version="1.0" encoding="utf-8"?>
2
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
3
+    android:layout_width="match_parent"
4
+    android:layout_height="match_parent"
5
+    android:background="@color/background_white">
6
+
7
+    <LinearLayout
8
+        android:id="@+id/title_bar_with_back_btn"
9
+        android:layout_width="match_parent"
10
+        android:layout_height="@dimen/action_bar_height"
11
+        android:background="@color/colorPrimary"
12
+        android:orientation="horizontal">
13
+
14
+        <LinearLayout
15
+            android:id="@+id/title_bar_back_layout"
16
+            android:layout_width="70dp"
17
+            android:layout_height="match_parent"
18
+            android:gravity="center_vertical"
19
+            android:orientation="horizontal"
20
+            android:paddingLeft="12dp">
21
+
22
+            <ImageView
23
+                android:layout_width="32dp"
24
+                android:layout_height="32dp"
25
+                android:src="@drawable/back_selector" />
26
+
27
+        </LinearLayout>
28
+
29
+        <TextView
30
+            android:id="@+id/title_bar_middle_txt"
31
+            android:layout_width="0dp"
32
+            android:layout_height="match_parent"
33
+            android:layout_weight="1"
34
+            android:gravity="center"
35
+            android:paddingLeft="10dp"
36
+            android:paddingRight="10dp"
37
+            android:text="@string/upload_settings"
38
+            android:textColor="@color/text_white"
39
+            android:textSize="@dimen/action_bar_title_medium_text_size" />
40
+
41
+        <LinearLayout
42
+            android:id="@+id/title_bar_option_layout"
43
+            android:layout_width="70dp"
44
+            android:layout_height="match_parent"
45
+            android:gravity="center_vertical|right"
46
+            android:visibility="invisible"
47
+            android:orientation="horizontal"
48
+            android:paddingRight="9dp">
49
+
50
+
51
+            <ImageView
52
+                android:id="@+id/iv_options"
53
+                android:layout_width="32dp"
54
+                android:layout_height="32dp"
55
+                android:layout_marginLeft="6dp"
56
+                android:src="@drawable/option" />
57
+
58
+        </LinearLayout>
59
+
60
+    </LinearLayout>
61
+
62
+    <android.support.v7.widget.RecyclerView
63
+        android:id="@+id/recycler_view_sessions"
64
+        android:layout_width="match_parent"
65
+        android:layout_height="match_parent"
66
+        android:layout_below="@id/title_bar_with_back_btn"/>
67
+
68
+</RelativeLayout>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -171,4 +171,6 @@
171 171
     <string name="upload_slow">长</string>
172 172
     <string name="upload_normal">中</string>
173 173
     <string name="upload_fast">短</string>
174
+
175
+    <string name="upload_settings">上传管理</string>
174 176
 </resources>

+ 225 - 0
views/src/main/java/com/android/views/stickylistheaders/AdapterWrapper.java

@@ -0,0 +1,225 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.content.Context;
4
+import android.database.DataSetObserver;
5
+import android.graphics.drawable.Drawable;
6
+import android.view.View;
7
+import android.view.View.OnClickListener;
8
+import android.view.ViewGroup;
9
+import android.widget.BaseAdapter;
10
+import android.widget.Checkable;
11
+import android.widget.ListAdapter;
12
+
13
+import java.util.LinkedList;
14
+import java.util.List;
15
+
16
+/**
17
+ * A {@link ListAdapter} which wraps a {@link StickyListHeadersAdapter} and
18
+ * automatically handles wrapping the result of
19
+ * {@link StickyListHeadersAdapter#getView(int, View, ViewGroup)}
20
+ * and
21
+ * {@link StickyListHeadersAdapter#getHeaderView(int, View, ViewGroup)}
22
+ * appropriately.
23
+ *
24
+ * @author Jake Wharton (jakewharton@gmail.com)
25
+ */
26
+class AdapterWrapper extends BaseAdapter implements StickyListHeadersAdapter {
27
+
28
+	interface OnHeaderClickListener {
29
+		void onHeaderClick(View header, int itemPosition, long headerId);
30
+	}
31
+
32
+	StickyListHeadersAdapter mDelegate;
33
+	private final List<View> mHeaderCache = new LinkedList<View>();
34
+	private final Context mContext;
35
+	private Drawable mDivider;
36
+	private int mDividerHeight;
37
+	private OnHeaderClickListener mOnHeaderClickListener;
38
+	private DataSetObserver mDataSetObserver = new DataSetObserver() {
39
+
40
+		@Override
41
+		public void onInvalidated() {
42
+			mHeaderCache.clear();
43
+			AdapterWrapper.super.notifyDataSetInvalidated();
44
+		}
45
+		
46
+		@Override
47
+		public void onChanged() {
48
+			AdapterWrapper.super.notifyDataSetChanged();
49
+		}
50
+	};
51
+
52
+	AdapterWrapper(Context context,
53
+			StickyListHeadersAdapter delegate) {
54
+		this.mContext = context;
55
+		this.mDelegate = delegate;
56
+		delegate.registerDataSetObserver(mDataSetObserver);
57
+	}
58
+
59
+	void setDivider(Drawable divider, int dividerHeight) {
60
+		this.mDivider = divider;
61
+		this.mDividerHeight = dividerHeight;
62
+		notifyDataSetChanged();
63
+	}
64
+
65
+	@Override
66
+	public boolean areAllItemsEnabled() {
67
+		return mDelegate.areAllItemsEnabled();
68
+	}
69
+
70
+	@Override
71
+	public boolean isEnabled(int position) {
72
+		return mDelegate.isEnabled(position);
73
+	}
74
+
75
+	@Override
76
+	public int getCount() {
77
+		return mDelegate.getCount();
78
+	}
79
+
80
+	@Override
81
+	public Object getItem(int position) {
82
+		return mDelegate.getItem(position);
83
+	}
84
+
85
+	@Override
86
+	public long getItemId(int position) {
87
+		return mDelegate.getItemId(position);
88
+	}
89
+
90
+	@Override
91
+	public boolean hasStableIds() {
92
+		return mDelegate.hasStableIds();
93
+	}
94
+
95
+	@Override
96
+	public int getItemViewType(int position) {
97
+		return mDelegate.getItemViewType(position);
98
+	}
99
+
100
+	@Override
101
+	public int getViewTypeCount() {
102
+		return mDelegate.getViewTypeCount();
103
+	}
104
+
105
+	@Override
106
+	public boolean isEmpty() {
107
+		return mDelegate.isEmpty();
108
+	}
109
+
110
+	/**
111
+	 * Will recycle header from {@link WrapperView} if it exists
112
+	 */
113
+	private void recycleHeaderIfExists(WrapperView wv) {
114
+		View header = wv.mHeader;
115
+		if (header != null) {
116
+			// reset the headers visibility when adding it to the cache
117
+			header.setVisibility(View.VISIBLE);
118
+			mHeaderCache.add(header);
119
+		}
120
+	}
121
+
122
+	/**
123
+	 * Get a header view. This optionally pulls a header from the supplied
124
+	 * {@link WrapperView} and will also recycle the divider if it exists.
125
+	 */
126
+	private View configureHeader(WrapperView wv, final int position) {
127
+		View header = wv.mHeader == null ? popHeader() : wv.mHeader;
128
+		header = mDelegate.getHeaderView(position, header, wv);
129
+		if (header == null) {
130
+			throw new NullPointerException("Header view must not be null.");
131
+		}
132
+		//if the header isn't clickable, the listselector will be drawn on top of the header
133
+		header.setClickable(true);
134
+		header.setOnClickListener(new OnClickListener() {
135
+
136
+			@Override
137
+			public void onClick(View v) {
138
+				if(mOnHeaderClickListener != null){
139
+					long headerId = mDelegate.getHeaderId(position);
140
+					mOnHeaderClickListener.onHeaderClick(v, position, headerId);
141
+				}
142
+			}
143
+		});
144
+		return header;
145
+	}
146
+
147
+	private View popHeader() {
148
+		if(mHeaderCache.size() > 0) {
149
+			return mHeaderCache.remove(0);
150
+		}
151
+		return null;
152
+	}
153
+
154
+	/** Returns {@code true} if the previous position has the same header ID. */
155
+	private boolean previousPositionHasSameHeader(int position) {
156
+		return position != 0
157
+				&& mDelegate.getHeaderId(position) == mDelegate
158
+						.getHeaderId(position - 1);
159
+	}
160
+
161
+	@Override
162
+	public WrapperView getView(int position, View convertView, ViewGroup parent) {
163
+		WrapperView wv = (convertView == null) ? new WrapperView(mContext) : (WrapperView) convertView;
164
+		View item = mDelegate.getView(position, wv.mItem, parent);
165
+		View header = null;
166
+		if (previousPositionHasSameHeader(position)) {
167
+			recycleHeaderIfExists(wv);
168
+		} else {
169
+			header = configureHeader(wv, position);
170
+		}
171
+		if((item instanceof Checkable) && !(wv instanceof CheckableWrapperView)) {
172
+			// Need to create Checkable subclass of WrapperView for ListView to work correctly
173
+			wv = new CheckableWrapperView(mContext);
174
+		} else if(!(item instanceof Checkable) && (wv instanceof CheckableWrapperView)) {
175
+			wv = new WrapperView(mContext);
176
+		}
177
+		wv.update(item, header, mDivider, mDividerHeight);
178
+		return wv;
179
+	}
180
+
181
+	public void setOnHeaderClickListener(OnHeaderClickListener onHeaderClickListener){
182
+		this.mOnHeaderClickListener = onHeaderClickListener;
183
+	}
184
+
185
+	@Override
186
+	public boolean equals(Object o) {
187
+		return mDelegate.equals(o); 
188
+	}
189
+
190
+	@Override
191
+	public View getDropDownView(int position, View convertView, ViewGroup parent) {
192
+		return ((BaseAdapter) mDelegate).getDropDownView(position, convertView, parent);
193
+	}
194
+
195
+	@Override
196
+	public int hashCode() {
197
+		return mDelegate.hashCode();
198
+	}
199
+
200
+	@Override
201
+	public void notifyDataSetChanged() {
202
+		((BaseAdapter) mDelegate).notifyDataSetChanged();
203
+	}
204
+
205
+	@Override
206
+	public void notifyDataSetInvalidated() {
207
+		((BaseAdapter) mDelegate).notifyDataSetInvalidated();
208
+	}
209
+
210
+	@Override
211
+	public String toString() {
212
+		return mDelegate.toString();
213
+	}
214
+
215
+	@Override
216
+	public View getHeaderView(int position, View convertView, ViewGroup parent) {
217
+		return mDelegate.getHeaderView(position, convertView, parent);
218
+	}
219
+
220
+	@Override
221
+	public long getHeaderId(int position) {
222
+		return mDelegate.getHeaderId(position);
223
+	}
224
+
225
+}

+ 31 - 0
views/src/main/java/com/android/views/stickylistheaders/CheckableWrapperView.java

@@ -0,0 +1,31 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.content.Context;
4
+import android.widget.Checkable;
5
+
6
+/**
7
+ * A WrapperView that implements the checkable interface
8
+ * 
9
+ * @author Emil Sjölander
10
+ */
11
+class CheckableWrapperView extends WrapperView implements Checkable {
12
+
13
+	public CheckableWrapperView(final Context context) {
14
+		super(context);
15
+	}
16
+
17
+	@Override
18
+	public boolean isChecked() {
19
+		return ((Checkable) mItem).isChecked();
20
+	}
21
+
22
+	@Override
23
+	public void setChecked(final boolean checked) {
24
+		((Checkable) mItem).setChecked(checked);
25
+	}
26
+
27
+	@Override
28
+	public void toggle() {
29
+		setChecked(!isChecked());
30
+	}
31
+}

+ 147 - 0
views/src/main/java/com/android/views/stickylistheaders/DistinctMultiHashMap.java

@@ -0,0 +1,147 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import java.util.ArrayList;
4
+import java.util.LinkedHashMap;
5
+import java.util.List;
6
+import java.util.Map;
7
+import java.util.Set;
8
+
9
+/**
10
+ * a hash map can maintain an one-to-many relationship which the value only belongs to one “one” part
11
+ * and the map also support getKey by value quickly
12
+ *
13
+ * @author lsjwzh
14
+ */
15
+class DistinctMultiHashMap<TKey,TItemValue> {
16
+    private IDMapper<TKey, TItemValue> mIDMapper;
17
+
18
+    interface IDMapper<TKey,TItemValue>{
19
+        public Object keyToKeyId(TKey key);
20
+        public TKey keyIdToKey(Object keyId);
21
+        public Object valueToValueId(TItemValue value);
22
+        public TItemValue valueIdToValue(Object valueId);
23
+    }
24
+
25
+    LinkedHashMap<Object,List<TItemValue>> mKeyToValuesMap = new LinkedHashMap<Object, List<TItemValue>>();
26
+    LinkedHashMap<Object,TKey> mValueToKeyIndexer = new LinkedHashMap<Object, TKey>();
27
+
28
+    DistinctMultiHashMap(){
29
+         this(new IDMapper<TKey, TItemValue>() {
30
+             @Override
31
+             public Object keyToKeyId(TKey key) {
32
+                 return key;
33
+             }
34
+
35
+             @Override
36
+             public TKey keyIdToKey(Object keyId) {
37
+                 return (TKey) keyId;
38
+             }
39
+
40
+             @Override
41
+             public Object valueToValueId(TItemValue value) {
42
+                 return value;
43
+             }
44
+
45
+             @Override
46
+             public TItemValue valueIdToValue(Object valueId) {
47
+                 return (TItemValue) valueId;
48
+             }
49
+         });
50
+    }
51
+    DistinctMultiHashMap(IDMapper<TKey, TItemValue> idMapper){
52
+        mIDMapper = idMapper;
53
+    }
54
+
55
+    public List<TItemValue> get(TKey key){
56
+        //todo immutable
57
+        return mKeyToValuesMap.get(mIDMapper.keyToKeyId(key));
58
+    }
59
+    public TKey getKey(TItemValue value){
60
+        return mValueToKeyIndexer.get(mIDMapper.valueToValueId(value));
61
+    }
62
+
63
+    public void add(TKey key,TItemValue value){
64
+        Object keyId = mIDMapper.keyToKeyId(key);
65
+        if(mKeyToValuesMap.get(keyId)==null){
66
+            mKeyToValuesMap.put(keyId,new ArrayList<TItemValue>());
67
+        }
68
+        //remove old relationship
69
+        TKey keyForValue = getKey(value);
70
+        if(keyForValue !=null){
71
+            mKeyToValuesMap.get(mIDMapper.keyToKeyId(keyForValue)).remove(value);
72
+        }
73
+        mValueToKeyIndexer.put(mIDMapper.valueToValueId(value), key);
74
+        if(!containsValue(mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)),value)) {
75
+            mKeyToValuesMap.get(mIDMapper.keyToKeyId(key)).add(value);
76
+        }
77
+    }
78
+
79
+    public void removeKey(TKey key){
80
+        if(mKeyToValuesMap.get(mIDMapper.keyToKeyId(key))!=null){
81
+            for (TItemValue value : mKeyToValuesMap.get(mIDMapper.keyToKeyId(key))){
82
+                mValueToKeyIndexer.remove(mIDMapper.valueToValueId(value));
83
+            }
84
+            mKeyToValuesMap.remove(mIDMapper.keyToKeyId(key));
85
+        }
86
+    }
87
+    public void removeValue(TItemValue value){
88
+        if(getKey(value)!=null){
89
+            List<TItemValue> itemValues = mKeyToValuesMap.get(mIDMapper.keyToKeyId(getKey(value)));
90
+            if(itemValues!=null){
91
+                itemValues.remove(value);
92
+            }
93
+        }
94
+        mValueToKeyIndexer.remove(mIDMapper.valueToValueId(value));
95
+    }
96
+
97
+    public void clear(){
98
+        mValueToKeyIndexer.clear();
99
+        mKeyToValuesMap.clear();
100
+    }
101
+
102
+    public void clearValues(){
103
+        for (Map.Entry<Object,List<TItemValue>> entry:entrySet()){
104
+            if(entry.getValue()!=null){
105
+                entry.getValue().clear();
106
+            }
107
+        }
108
+        mValueToKeyIndexer.clear();
109
+    }
110
+
111
+    public Set<Map.Entry<Object,List<TItemValue>>> entrySet(){
112
+        return mKeyToValuesMap.entrySet();
113
+    }
114
+
115
+    public Set<Map.Entry<Object,TKey>> reverseEntrySet(){
116
+        return mValueToKeyIndexer.entrySet();
117
+    }
118
+
119
+    public int size(){
120
+        return mKeyToValuesMap.size();
121
+    }
122
+    public int valuesSize(){
123
+        return mValueToKeyIndexer.size();
124
+    }
125
+
126
+    protected boolean containsValue(List<TItemValue> list,TItemValue  value){
127
+        for (TItemValue itemValue :list){
128
+            if(mIDMapper.valueToValueId(itemValue).equals(mIDMapper.valueToValueId(value))){
129
+                return true;
130
+            }
131
+        }
132
+        return false;
133
+    }
134
+
135
+    /**
136
+     * @param position
137
+     * @return
138
+     */
139
+    public TItemValue getValueByPosition(int position){
140
+        Object[] vauleIdArray = mValueToKeyIndexer.keySet().toArray();
141
+        if(position>vauleIdArray.length){
142
+            throw new IndexOutOfBoundsException();
143
+        }
144
+        Object valueId = vauleIdArray[position];
145
+        return mIDMapper.valueIdToValue(valueId);
146
+    }
147
+}

+ 39 - 0
views/src/main/java/com/android/views/stickylistheaders/DualHashMap.java

@@ -0,0 +1,39 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import java.util.HashMap;
4
+
5
+/**
6
+ * simple two way hashmap
7
+ * @author lsjwzh
8
+ */
9
+class DualHashMap<TKey, TValue> {
10
+    HashMap<TKey, TValue> mKeyToValue = new HashMap<TKey, TValue>();
11
+    HashMap<TValue, TKey> mValueToKey = new HashMap<TValue, TKey>();
12
+
13
+    public void put(TKey t1, TValue t2){
14
+        remove(t1);
15
+        removeByValue(t2);
16
+        mKeyToValue.put(t1, t2);
17
+        mValueToKey.put(t2, t1);
18
+    }
19
+
20
+    public TKey getKey(TValue value){
21
+        return mValueToKey.get(value);
22
+    }
23
+    public TValue get(TKey key){
24
+        return mKeyToValue.get(key);
25
+    }
26
+
27
+    public void remove(TKey key){
28
+        if(get(key)!=null){
29
+            mValueToKey.remove(get(key));
30
+        }
31
+        mKeyToValue.remove(key);
32
+    }
33
+    public void removeByValue(TValue value){
34
+        if(getKey(value)!=null){
35
+            mKeyToValue.remove(getKey(value));
36
+        }
37
+        mValueToKey.remove(value);
38
+    }
39
+}

+ 131 - 0
views/src/main/java/com/android/views/stickylistheaders/ExpandableStickyListHeadersAdapter.java

@@ -0,0 +1,131 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.database.DataSetObserver;
4
+import android.view.View;
5
+import android.view.ViewGroup;
6
+import android.widget.BaseAdapter;
7
+
8
+import java.util.ArrayList;
9
+import java.util.List;
10
+
11
+
12
+/**
13
+ * @author lsjwzh
14
+ */
15
+ class ExpandableStickyListHeadersAdapter extends BaseAdapter implements StickyListHeadersAdapter {
16
+
17
+    private final StickyListHeadersAdapter mInnerAdapter;
18
+    DualHashMap<View,Long> mViewToItemIdMap = new DualHashMap<View, Long>();
19
+    DistinctMultiHashMap<Integer,View> mHeaderIdToViewMap = new DistinctMultiHashMap<Integer, View>();
20
+    List<Long> mCollapseHeaderIds = new ArrayList<Long>();
21
+
22
+    ExpandableStickyListHeadersAdapter(StickyListHeadersAdapter innerAdapter){
23
+        this.mInnerAdapter = innerAdapter;
24
+    }
25
+
26
+    @Override
27
+    public View getHeaderView(int position, View convertView, ViewGroup parent) {
28
+        return mInnerAdapter.getHeaderView(position,convertView,parent);
29
+    }
30
+
31
+    @Override
32
+    public long getHeaderId(int position) {
33
+        return mInnerAdapter.getHeaderId(position);
34
+    }
35
+
36
+    @Override
37
+    public boolean areAllItemsEnabled() {
38
+        return mInnerAdapter.areAllItemsEnabled();
39
+    }
40
+
41
+    @Override
42
+    public boolean isEnabled(int i) {
43
+        return mInnerAdapter.isEnabled(i);
44
+    }
45
+
46
+    @Override
47
+    public void registerDataSetObserver(DataSetObserver dataSetObserver) {
48
+        mInnerAdapter.registerDataSetObserver(dataSetObserver);
49
+    }
50
+
51
+    @Override
52
+    public void unregisterDataSetObserver(DataSetObserver dataSetObserver) {
53
+        mInnerAdapter.unregisterDataSetObserver(dataSetObserver);
54
+    }
55
+
56
+    @Override
57
+    public int getCount() {
58
+        return mInnerAdapter.getCount();
59
+    }
60
+
61
+    @Override
62
+    public Object getItem(int i) {
63
+        return mInnerAdapter.getItem(i);
64
+    }
65
+
66
+    @Override
67
+    public long getItemId(int i) {
68
+        return mInnerAdapter.getItemId(i);
69
+    }
70
+
71
+    @Override
72
+    public boolean hasStableIds() {
73
+        return mInnerAdapter.hasStableIds();
74
+    }
75
+
76
+    @Override
77
+    public View getView(int i, View view, ViewGroup viewGroup) {
78
+        View convertView = mInnerAdapter.getView(i,view,viewGroup);
79
+        mViewToItemIdMap.put(convertView, getItemId(i));
80
+        mHeaderIdToViewMap.add((int) getHeaderId(i), convertView);
81
+        if(mCollapseHeaderIds.contains(getHeaderId(i))){
82
+            convertView.setVisibility(View.GONE);
83
+        }else {
84
+            convertView.setVisibility(View.VISIBLE);
85
+        }
86
+        return convertView;
87
+    }
88
+
89
+    @Override
90
+    public int getItemViewType(int i) {
91
+        return mInnerAdapter.getItemViewType(i);
92
+    }
93
+
94
+    @Override
95
+    public int getViewTypeCount() {
96
+        return mInnerAdapter.getViewTypeCount();
97
+    }
98
+
99
+    @Override
100
+    public boolean isEmpty() {
101
+        return mInnerAdapter.isEmpty();
102
+    }
103
+
104
+    public List<View> getItemViewsByHeaderId(long headerId){
105
+        return mHeaderIdToViewMap.get((int) headerId);
106
+    }
107
+
108
+    public boolean isHeaderCollapsed(long headerId){
109
+        return mCollapseHeaderIds.contains(headerId);
110
+    }
111
+
112
+    public void expand(long headerId) {
113
+        if(isHeaderCollapsed(headerId)){
114
+            mCollapseHeaderIds.remove((Object) headerId);
115
+        }
116
+    }
117
+
118
+    public void collapse(long headerId) {
119
+        if(!isHeaderCollapsed(headerId)){
120
+            mCollapseHeaderIds.add(headerId);
121
+        }
122
+    }
123
+
124
+    public View findViewByItemId(long itemId){
125
+         return mViewToItemIdMap.getKey(itemId);
126
+    }
127
+
128
+    public long findItemIdByView(View view){
129
+        return mViewToItemIdMap.get(view);
130
+    }
131
+}

+ 126 - 0
views/src/main/java/com/android/views/stickylistheaders/ExpandableStickyListHeadersListView.java

@@ -0,0 +1,126 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.content.Context;
4
+import android.util.AttributeSet;
5
+import android.view.View;
6
+
7
+import java.util.List;
8
+
9
+/**
10
+ * add expand/collapse functions like ExpandableListView
11
+ * @author lsjwzh
12
+ */
13
+public class ExpandableStickyListHeadersListView extends StickyListHeadersListView {
14
+    public interface IAnimationExecutor{
15
+        public void executeAnim(View target, int animType);
16
+    }
17
+
18
+    public final static int ANIMATION_COLLAPSE = 1;
19
+    public final static int ANIMATION_EXPAND = 0;
20
+
21
+    ExpandableStickyListHeadersAdapter mExpandableStickyListHeadersAdapter;
22
+
23
+
24
+
25
+    IAnimationExecutor mDefaultAnimExecutor = new IAnimationExecutor() {
26
+        @Override
27
+        public void executeAnim(View target, int animType) {
28
+            if(animType==ANIMATION_EXPAND){
29
+                target.setVisibility(VISIBLE);
30
+            }else if(animType==ANIMATION_COLLAPSE){
31
+                target.setVisibility(GONE);
32
+            }
33
+        }
34
+    };
35
+
36
+
37
+    public ExpandableStickyListHeadersListView(Context context) {
38
+        super(context);
39
+    }
40
+
41
+    public ExpandableStickyListHeadersListView(Context context, AttributeSet attrs) {
42
+        super(context, attrs);
43
+    }
44
+
45
+    public ExpandableStickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) {
46
+        super(context, attrs, defStyle);
47
+    }
48
+
49
+    @Override
50
+    public ExpandableStickyListHeadersAdapter getAdapter() {
51
+        return mExpandableStickyListHeadersAdapter;
52
+    }
53
+
54
+    @Override
55
+    public void setAdapter(StickyListHeadersAdapter adapter) {
56
+        mExpandableStickyListHeadersAdapter = new ExpandableStickyListHeadersAdapter(adapter);
57
+        super.setAdapter(mExpandableStickyListHeadersAdapter);
58
+    }
59
+
60
+    public View findViewByItemId(long itemId){
61
+        return mExpandableStickyListHeadersAdapter.findViewByItemId(itemId);
62
+    }
63
+
64
+    public long findItemIdByView(View view){
65
+        return mExpandableStickyListHeadersAdapter.findItemIdByView(view);
66
+    }
67
+
68
+    public void expand(long headerId) {
69
+        if(!mExpandableStickyListHeadersAdapter.isHeaderCollapsed(headerId)){
70
+            return;
71
+        }
72
+        mExpandableStickyListHeadersAdapter.expand(headerId);
73
+        //find and expand views in group
74
+        List<View> itemViews = mExpandableStickyListHeadersAdapter.getItemViewsByHeaderId(headerId);
75
+        if(itemViews==null){
76
+            return;
77
+        }
78
+        for (View view : itemViews) {
79
+            animateView(view, ANIMATION_EXPAND);
80
+        }
81
+    }
82
+
83
+    public void collapse(long headerId) {
84
+        if(mExpandableStickyListHeadersAdapter.isHeaderCollapsed(headerId)){
85
+            return;
86
+        }
87
+        mExpandableStickyListHeadersAdapter.collapse(headerId);
88
+        //find and hide views with the same header
89
+        List<View> itemViews = mExpandableStickyListHeadersAdapter.getItemViewsByHeaderId(headerId);
90
+        if(itemViews==null){
91
+            return;
92
+        }
93
+        for (View view : itemViews) {
94
+            animateView(view, ANIMATION_COLLAPSE);
95
+        }
96
+    }
97
+
98
+    public boolean isHeaderCollapsed(long headerId){
99
+        return  mExpandableStickyListHeadersAdapter.isHeaderCollapsed(headerId);
100
+    }
101
+
102
+    public void setAnimExecutor(IAnimationExecutor animExecutor) {
103
+        this.mDefaultAnimExecutor = animExecutor;
104
+    }
105
+
106
+    /**
107
+     * Performs either COLLAPSE or EXPAND animation on the target view
108
+     *
109
+     * @param target the view to animate
110
+     * @param type   the animation type, either ExpandCollapseAnimation.COLLAPSE
111
+     *               or ExpandCollapseAnimation.EXPAND
112
+     */
113
+    private void animateView(final View target, final int type) {
114
+        if(ANIMATION_EXPAND==type&&target.getVisibility()==VISIBLE){
115
+            return;
116
+        }
117
+        if(ANIMATION_COLLAPSE==type&&target.getVisibility()!=VISIBLE){
118
+            return;
119
+        }
120
+        if(mDefaultAnimExecutor !=null){
121
+            mDefaultAnimExecutor.executeAnim(target,type);
122
+        }
123
+
124
+    }
125
+
126
+}

+ 32 - 0
views/src/main/java/com/android/views/stickylistheaders/SectionIndexerAdapterWrapper.java

@@ -0,0 +1,32 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.content.Context;
4
+import android.widget.SectionIndexer;
5
+
6
+class SectionIndexerAdapterWrapper extends
7
+		AdapterWrapper implements SectionIndexer {
8
+	
9
+	SectionIndexer mSectionIndexerDelegate;
10
+
11
+	SectionIndexerAdapterWrapper(Context context,
12
+			StickyListHeadersAdapter delegate) {
13
+		super(context, delegate);
14
+		mSectionIndexerDelegate = (SectionIndexer) delegate;
15
+	}
16
+
17
+	@Override
18
+	public int getPositionForSection(int section) {
19
+		return mSectionIndexerDelegate.getPositionForSection(section);
20
+	}
21
+
22
+	@Override
23
+	public int getSectionForPosition(int position) {
24
+		return mSectionIndexerDelegate.getSectionForPosition(position);
25
+	}
26
+
27
+	@Override
28
+	public Object[] getSections() {
29
+		return mSectionIndexerDelegate.getSections();
30
+	}
31
+
32
+}

+ 38 - 0
views/src/main/java/com/android/views/stickylistheaders/StickyListHeadersAdapter.java

@@ -0,0 +1,38 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.view.View;
4
+import android.view.ViewGroup;
5
+import android.widget.ListAdapter;
6
+
7
+public interface StickyListHeadersAdapter extends ListAdapter {
8
+	/**
9
+	 * Get a View that displays the header data at the specified position in the
10
+	 * set. You can either create a View manually or inflate it from an XML layout
11
+	 * file.
12
+	 *
13
+	 * @param position
14
+	 * The position of the item within the adapter's data set of the item whose
15
+	 * header view we want.
16
+	 * @param convertView
17
+	 * The old view to reuse, if possible. Note: You should check that this view is
18
+	 * non-null and of an appropriate type before using. If it is not possible to
19
+	 * convert this view to display the correct data, this method can create a new
20
+	 * view.
21
+	 * @param parent
22
+	 * The parent that this view will eventually be attached to.
23
+	 * @return
24
+	 * A View corresponding to the data at the specified position.
25
+	 */
26
+	View getHeaderView(int position, View convertView, ViewGroup parent);
27
+
28
+	/**
29
+	 * Get the header id associated with the specified position in the list.
30
+	 *
31
+	 * @param position
32
+	 * The position of the item within the adapter's data set whose header id we
33
+	 * want.
34
+	 * @return
35
+	 * The id of the header at the specified position.
36
+	 */
37
+	long getHeaderId(int position);
38
+}

+ 1131 - 0
views/src/main/java/com/android/views/stickylistheaders/StickyListHeadersListView.java

@@ -0,0 +1,1131 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.annotation.SuppressLint;
4
+import android.annotation.TargetApi;
5
+import android.content.Context;
6
+import android.content.res.TypedArray;
7
+import android.database.DataSetObserver;
8
+import android.graphics.Canvas;
9
+import android.graphics.drawable.Drawable;
10
+import android.os.Build;
11
+import android.os.Parcelable;
12
+import android.util.AttributeSet;
13
+import android.util.Log;
14
+import android.util.SparseBooleanArray;
15
+import android.view.MotionEvent;
16
+import android.view.View;
17
+import android.view.ViewConfiguration;
18
+import android.view.ViewGroup;
19
+import android.widget.AbsListView;
20
+import android.widget.AbsListView.MultiChoiceModeListener;
21
+import android.widget.AbsListView.OnScrollListener;
22
+import android.widget.AdapterView.OnItemClickListener;
23
+import android.widget.AdapterView.OnItemLongClickListener;
24
+import android.widget.FrameLayout;
25
+import android.widget.ListView;
26
+import android.widget.SectionIndexer;
27
+
28
+import com.android.views.R;
29
+import com.android.views.stickylistheaders.WrapperViewList.LifeCycleListener;
30
+
31
+/**
32
+ * Even though this is a FrameLayout subclass we still consider it a ListView.
33
+ * This is because of 2 reasons:
34
+ *   1. It acts like as ListView.
35
+ *   2. It used to be a ListView subclass and refactoring the name would cause compatibility errors.
36
+ *
37
+ * @author Emil Sjölander
38
+ */
39
+public class StickyListHeadersListView extends FrameLayout {
40
+
41
+    public interface OnHeaderClickListener {
42
+        void onHeaderClick(StickyListHeadersListView l, View header,
43
+                           int itemPosition, long headerId, boolean currentlySticky);
44
+    }
45
+
46
+    /**
47
+     * Notifies the listener when the sticky headers top offset has changed.
48
+     */
49
+    public interface OnStickyHeaderOffsetChangedListener {
50
+        /**
51
+         * @param l      The view parent
52
+         * @param header The currently sticky header being offset.
53
+         *               This header is not guaranteed to have it's measurements set.
54
+         *               It is however guaranteed that this view has been measured,
55
+         *               therefor you should user getMeasured* methods instead of
56
+         *               get* methods for determining the view's size.
57
+         * @param offset The amount the sticky header is offset by towards to top of the screen.
58
+         */
59
+        void onStickyHeaderOffsetChanged(StickyListHeadersListView l, View header, int offset);
60
+    }
61
+
62
+    /**
63
+     * Notifies the listener when the sticky header has been updated
64
+     */
65
+    public interface OnStickyHeaderChangedListener {
66
+        /**
67
+         * @param l             The view parent
68
+         * @param header        The new sticky header view.
69
+         * @param itemPosition  The position of the item within the adapter's data set of
70
+         *                      the item whose header is now sticky.
71
+         * @param headerId      The id of the new sticky header.
72
+         */
73
+        void onStickyHeaderChanged(StickyListHeadersListView l, View header,
74
+                                   int itemPosition, long headerId);
75
+
76
+    }
77
+
78
+    /* --- Children --- */
79
+    private WrapperViewList mList;
80
+    private View mHeader;
81
+
82
+    /* --- Header state --- */
83
+    private Long mHeaderId;
84
+    // used to not have to call getHeaderId() all the time
85
+    private Integer mHeaderPosition;
86
+    private Integer mHeaderOffset;
87
+
88
+    /* --- Delegates --- */
89
+    private OnScrollListener mOnScrollListenerDelegate;
90
+    private AdapterWrapper mAdapter;
91
+
92
+    /* --- Settings --- */
93
+    private boolean mAreHeadersSticky = true;
94
+    private boolean mClippingToPadding = true;
95
+    private boolean mIsDrawingListUnderStickyHeader = true;
96
+    private int mStickyHeaderTopOffset = 0;
97
+    private int mPaddingLeft = 0;
98
+    private int mPaddingTop = 0;
99
+    private int mPaddingRight = 0;
100
+    private int mPaddingBottom = 0;
101
+
102
+    /* --- Touch handling --- */
103
+    private float mDownY;
104
+    private boolean mHeaderOwnsTouch;
105
+    private float mTouchSlop;
106
+
107
+    /* --- Other --- */
108
+    private OnHeaderClickListener mOnHeaderClickListener;
109
+    private OnStickyHeaderOffsetChangedListener mOnStickyHeaderOffsetChangedListener;
110
+    private OnStickyHeaderChangedListener mOnStickyHeaderChangedListener;
111
+    private AdapterWrapperDataSetObserver mDataSetObserver;
112
+    private Drawable mDivider;
113
+    private int mDividerHeight;
114
+
115
+    public StickyListHeadersListView(Context context) {
116
+        this(context, null);
117
+    }
118
+
119
+    public StickyListHeadersListView(Context context, AttributeSet attrs) {
120
+        this(context, attrs, R.attr.stickyListHeadersListViewStyle);
121
+    }
122
+
123
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
124
+    public StickyListHeadersListView(Context context, AttributeSet attrs, int defStyle) {
125
+        super(context, attrs, defStyle);
126
+
127
+        mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
128
+
129
+        // Initialize the wrapped list
130
+        mList = new WrapperViewList(context);
131
+
132
+        // null out divider, dividers are handled by adapter so they look good with headers
133
+        mDivider = mList.getDivider();
134
+        mDividerHeight = mList.getDividerHeight();
135
+        mList.setDivider(null);
136
+        mList.setDividerHeight(0);
137
+
138
+        if (attrs != null) {
139
+            TypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.StickyListHeadersListView, defStyle, 0);
140
+
141
+            try {
142
+                // -- View attributes --
143
+                int padding = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_padding, 0);
144
+                mPaddingLeft = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingLeft, padding);
145
+                mPaddingTop = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingTop, padding);
146
+                mPaddingRight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingRight, padding);
147
+                mPaddingBottom = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_paddingBottom, padding);
148
+
149
+                setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom);
150
+
151
+                // Set clip to padding on the list and reset value to default on
152
+                // wrapper
153
+                mClippingToPadding = a.getBoolean(R.styleable.StickyListHeadersListView_android_clipToPadding, true);
154
+                super.setClipToPadding(true);
155
+                mList.setClipToPadding(mClippingToPadding);
156
+
157
+                // scrollbars
158
+                final int scrollBars = a.getInt(R.styleable.StickyListHeadersListView_android_scrollbars, 0x00000200);
159
+                mList.setVerticalScrollBarEnabled((scrollBars & 0x00000200) != 0);
160
+                mList.setHorizontalScrollBarEnabled((scrollBars & 0x00000100) != 0);
161
+
162
+                // overscroll
163
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
164
+                    mList.setOverScrollMode(a.getInt(R.styleable.StickyListHeadersListView_android_overScrollMode, 0));
165
+                }
166
+
167
+                // -- ListView attributes --
168
+                mList.setFadingEdgeLength(a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_fadingEdgeLength,
169
+                        mList.getVerticalFadingEdgeLength()));
170
+                final int fadingEdge = a.getInt(R.styleable.StickyListHeadersListView_android_requiresFadingEdge, 0);
171
+                if (fadingEdge == 0x00001000) {
172
+                    mList.setVerticalFadingEdgeEnabled(false);
173
+                    mList.setHorizontalFadingEdgeEnabled(true);
174
+                } else if (fadingEdge == 0x00002000) {
175
+                    mList.setVerticalFadingEdgeEnabled(true);
176
+                    mList.setHorizontalFadingEdgeEnabled(false);
177
+                } else {
178
+                    mList.setVerticalFadingEdgeEnabled(false);
179
+                    mList.setHorizontalFadingEdgeEnabled(false);
180
+                }
181
+                mList.setCacheColorHint(a
182
+                        .getColor(R.styleable.StickyListHeadersListView_android_cacheColorHint, mList.getCacheColorHint()));
183
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
184
+                    mList.setChoiceMode(a.getInt(R.styleable.StickyListHeadersListView_android_choiceMode,
185
+                            mList.getChoiceMode()));
186
+                }
187
+                mList.setDrawSelectorOnTop(a.getBoolean(R.styleable.StickyListHeadersListView_android_drawSelectorOnTop, false));
188
+                mList.setFastScrollEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_fastScrollEnabled,
189
+                        mList.isFastScrollEnabled()));
190
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
191
+                    mList.setFastScrollAlwaysVisible(a.getBoolean(
192
+                            R.styleable.StickyListHeadersListView_android_fastScrollAlwaysVisible,
193
+                            mList.isFastScrollAlwaysVisible()));
194
+                }
195
+
196
+                mList.setScrollBarStyle(a.getInt(R.styleable.StickyListHeadersListView_android_scrollbarStyle, 0));
197
+
198
+                if (a.hasValue(R.styleable.StickyListHeadersListView_android_listSelector)) {
199
+                    mList.setSelector(a.getDrawable(R.styleable.StickyListHeadersListView_android_listSelector));
200
+                }
201
+
202
+                mList.setScrollingCacheEnabled(a.getBoolean(R.styleable.StickyListHeadersListView_android_scrollingCache,
203
+                        mList.isScrollingCacheEnabled()));
204
+
205
+                if (a.hasValue(R.styleable.StickyListHeadersListView_android_divider)) {
206
+                    mDivider = a.getDrawable(R.styleable.StickyListHeadersListView_android_divider);
207
+                }
208
+                
209
+                mList.setStackFromBottom(a.getBoolean(R.styleable.StickyListHeadersListView_android_stackFromBottom, false));
210
+
211
+                mDividerHeight = a.getDimensionPixelSize(R.styleable.StickyListHeadersListView_android_dividerHeight,
212
+                        mDividerHeight);
213
+
214
+                mList.setTranscriptMode(a.getInt(R.styleable.StickyListHeadersListView_android_transcriptMode,
215
+                        ListView.TRANSCRIPT_MODE_DISABLED));
216
+
217
+                // -- StickyListHeaders attributes --
218
+                mAreHeadersSticky = a.getBoolean(R.styleable.StickyListHeadersListView_hasStickyHeaders, true);
219
+                mIsDrawingListUnderStickyHeader = a.getBoolean(
220
+                        R.styleable.StickyListHeadersListView_isDrawingListUnderStickyHeader,
221
+                        true);
222
+            } finally {
223
+                a.recycle();
224
+            }
225
+        }
226
+
227
+        // attach some listeners to the wrapped list
228
+        mList.setLifeCycleListener(new WrapperViewListLifeCycleListener());
229
+        mList.setOnScrollListener(new WrapperListScrollListener());
230
+
231
+        addView(mList);
232
+    }
233
+
234
+    @Override
235
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
236
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
237
+        measureHeader(mHeader);
238
+    }
239
+
240
+    private void ensureHeaderHasCorrectLayoutParams(View header) {
241
+        ViewGroup.LayoutParams lp = header.getLayoutParams();
242
+        if (lp == null) {
243
+            lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
244
+            header.setLayoutParams(lp);
245
+        } else if (lp.height == LayoutParams.MATCH_PARENT || lp.width == LayoutParams.WRAP_CONTENT) {
246
+            lp.height = LayoutParams.WRAP_CONTENT;
247
+            lp.width = LayoutParams.MATCH_PARENT;
248
+            header.setLayoutParams(lp);
249
+        }
250
+    }
251
+
252
+    private void measureHeader(View header) {
253
+        if (header != null) {
254
+            final int width = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
255
+            final int parentWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
256
+                    width, MeasureSpec.EXACTLY);
257
+            final int parentHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0,
258
+                    MeasureSpec.UNSPECIFIED);
259
+            measureChild(header, parentWidthMeasureSpec,
260
+                    parentHeightMeasureSpec);
261
+        }
262
+    }
263
+
264
+    @Override
265
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
266
+        mList.layout(0, 0, mList.getMeasuredWidth(), getHeight());
267
+        if (mHeader != null) {
268
+            MarginLayoutParams lp = (MarginLayoutParams) mHeader.getLayoutParams();
269
+            int headerTop = lp.topMargin;
270
+            mHeader.layout(mPaddingLeft, headerTop, mHeader.getMeasuredWidth()
271
+                    + mPaddingLeft, headerTop + mHeader.getMeasuredHeight());
272
+        }
273
+    }
274
+
275
+    @Override
276
+    protected void dispatchDraw(Canvas canvas) {
277
+        // Only draw the list here.
278
+        // The header should be drawn right after the lists children are drawn.
279
+        // This is done so that the header is above the list items
280
+        // but below the list decorators (scroll bars etc).
281
+        if (mList.getVisibility() == VISIBLE || mList.getAnimation() != null) {
282
+            drawChild(canvas, mList, 0);
283
+        }
284
+    }
285
+
286
+    // Reset values tied the header. also remove header form layout
287
+    // This is called in response to the data set or the adapter changing
288
+    private void clearHeader() {
289
+        if (mHeader != null) {
290
+            removeView(mHeader);
291
+            mHeader = null;
292
+            mHeaderId = null;
293
+            mHeaderPosition = null;
294
+            mHeaderOffset = null;
295
+
296
+            // reset the top clipping length
297
+            mList.setTopClippingLength(0);
298
+            updateHeaderVisibilities();
299
+        }
300
+    }
301
+
302
+    private void updateOrClearHeader(int firstVisiblePosition) {
303
+        final int adapterCount = mAdapter == null ? 0 : mAdapter.getCount();
304
+        if (adapterCount == 0 || !mAreHeadersSticky) {
305
+            return;
306
+        }
307
+
308
+        final int headerViewCount = mList.getHeaderViewsCount();
309
+        int headerPosition = firstVisiblePosition - headerViewCount;
310
+        if (mList.getChildCount() > 0) {
311
+            View firstItem = mList.getChildAt(0);
312
+            if (firstItem.getBottom() < stickyHeaderTop()) {
313
+                headerPosition++;
314
+            }
315
+        }
316
+
317
+        // It is not a mistake to call getFirstVisiblePosition() here.
318
+        // Most of the time getFixedFirstVisibleItem() should be called
319
+        // but that does not work great together with getChildAt()
320
+        final boolean doesListHaveChildren = mList.getChildCount() != 0;
321
+        final boolean isFirstViewBelowTop = doesListHaveChildren
322
+                && mList.getFirstVisiblePosition() == 0
323
+                && mList.getChildAt(0).getTop() >= stickyHeaderTop();
324
+        final boolean isHeaderPositionOutsideAdapterRange = headerPosition > adapterCount - 1
325
+                || headerPosition < 0;
326
+        if (!doesListHaveChildren || isHeaderPositionOutsideAdapterRange || isFirstViewBelowTop) {
327
+            clearHeader();
328
+            return;
329
+        }
330
+
331
+        updateHeader(headerPosition);
332
+    }
333
+
334
+    private void updateHeader(int headerPosition) {
335
+
336
+        // check if there is a new header should be sticky
337
+        if (mHeaderPosition == null || mHeaderPosition != headerPosition) {
338
+            mHeaderPosition = headerPosition;
339
+            final long headerId = mAdapter.getHeaderId(headerPosition);
340
+            if (mHeaderId == null || mHeaderId != headerId) {
341
+                mHeaderId = headerId;
342
+                final View header = mAdapter.getHeaderView(mHeaderPosition, mHeader, this);
343
+                if (mHeader != header) {
344
+                    if (header == null) {
345
+                        throw new NullPointerException("header may not be null");
346
+                    }
347
+                    swapHeader(header);
348
+                }
349
+                ensureHeaderHasCorrectLayoutParams(mHeader);
350
+                measureHeader(mHeader);
351
+                if(mOnStickyHeaderChangedListener != null) {
352
+                    mOnStickyHeaderChangedListener.onStickyHeaderChanged(this, mHeader, headerPosition, mHeaderId);
353
+                }
354
+                // Reset mHeaderOffset to null ensuring
355
+                // that it will be set on the header and
356
+                // not skipped for performance reasons.
357
+                mHeaderOffset = null;
358
+            }
359
+        }
360
+
361
+        int headerOffset = stickyHeaderTop();
362
+
363
+        // Calculate new header offset
364
+        // Skip looking at the first view. it never matters because it always
365
+        // results in a headerOffset = 0
366
+        for (int i = 0; i < mList.getChildCount(); i++) {
367
+            final View child = mList.getChildAt(i);
368
+            final boolean doesChildHaveHeader = child instanceof WrapperView && ((WrapperView) child).hasHeader();
369
+            final boolean isChildFooter = mList.containsFooterView(child);
370
+            if (child.getTop() >= stickyHeaderTop() && (doesChildHaveHeader || isChildFooter)) {
371
+                headerOffset = Math.min(child.getTop() - mHeader.getMeasuredHeight(), headerOffset);
372
+                break;
373
+            }
374
+        }
375
+
376
+        setHeaderOffet(headerOffset);
377
+
378
+        if (!mIsDrawingListUnderStickyHeader) {
379
+            mList.setTopClippingLength(mHeader.getMeasuredHeight()
380
+                    + mHeaderOffset);
381
+        }
382
+
383
+        updateHeaderVisibilities();
384
+    }
385
+
386
+    private void swapHeader(View newHeader) {
387
+        if (mHeader != null) {
388
+            removeView(mHeader);
389
+        }
390
+        mHeader = newHeader;
391
+        addView(mHeader);
392
+        if (mOnHeaderClickListener != null) {
393
+            mHeader.setOnClickListener(new OnClickListener() {
394
+                @Override
395
+                public void onClick(View v) {
396
+                    mOnHeaderClickListener.onHeaderClick(
397
+                            StickyListHeadersListView.this, mHeader,
398
+                            mHeaderPosition, mHeaderId, true);
399
+                }
400
+            });
401
+        }
402
+        mHeader.setClickable(true);
403
+    }
404
+
405
+    // hides the headers in the list under the sticky header.
406
+    // Makes sure the other ones are showing
407
+    private void updateHeaderVisibilities() {
408
+        int top = stickyHeaderTop();
409
+        int childCount = mList.getChildCount();
410
+        for (int i = 0; i < childCount; i++) {
411
+
412
+            // ensure child is a wrapper view
413
+            View child = mList.getChildAt(i);
414
+            if (!(child instanceof WrapperView)) {
415
+                continue;
416
+            }
417
+
418
+            // ensure wrapper view child has a header
419
+            WrapperView wrapperViewChild = (WrapperView) child;
420
+            if (!wrapperViewChild.hasHeader()) {
421
+                continue;
422
+            }
423
+
424
+            // update header views visibility
425
+            View childHeader = wrapperViewChild.mHeader;
426
+            if (wrapperViewChild.getTop() < top) {
427
+                if (childHeader.getVisibility() != View.INVISIBLE) {
428
+                    childHeader.setVisibility(View.INVISIBLE);
429
+                }
430
+            } else {
431
+                if (childHeader.getVisibility() != View.VISIBLE) {
432
+                    childHeader.setVisibility(View.VISIBLE);
433
+                }
434
+            }
435
+        }
436
+    }
437
+
438
+    // Wrapper around setting the header offset in different ways depending on
439
+    // the API version
440
+    @SuppressLint("NewApi")
441
+    private void setHeaderOffet(int offset) {
442
+        if (mHeaderOffset == null || mHeaderOffset != offset) {
443
+            mHeaderOffset = offset;
444
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
445
+                mHeader.setTranslationY(mHeaderOffset);
446
+            } else {
447
+                MarginLayoutParams params = (MarginLayoutParams) mHeader.getLayoutParams();
448
+                params.topMargin = mHeaderOffset;
449
+                mHeader.setLayoutParams(params);
450
+            }
451
+            if (mOnStickyHeaderOffsetChangedListener != null) {
452
+                mOnStickyHeaderOffsetChangedListener.onStickyHeaderOffsetChanged(this, mHeader, -mHeaderOffset);
453
+            }
454
+        }
455
+    }
456
+
457
+    @Override
458
+    public boolean dispatchTouchEvent(MotionEvent ev) {
459
+        int action = ev.getAction() & MotionEvent.ACTION_MASK;
460
+        if (action == MotionEvent.ACTION_DOWN) {
461
+            mDownY = ev.getY();
462
+            mHeaderOwnsTouch = mHeader != null && mDownY <= mHeader.getHeight() + mHeaderOffset;
463
+        }
464
+
465
+        boolean handled;
466
+        if (mHeaderOwnsTouch) {
467
+            if (mHeader != null && Math.abs(mDownY - ev.getY()) <= mTouchSlop) {
468
+                handled = mHeader.dispatchTouchEvent(ev);
469
+            } else {
470
+                if (mHeader != null) {
471
+                    MotionEvent cancelEvent = MotionEvent.obtain(ev);
472
+                    cancelEvent.setAction(MotionEvent.ACTION_CANCEL);
473
+                    mHeader.dispatchTouchEvent(cancelEvent);
474
+                    cancelEvent.recycle();
475
+                }
476
+
477
+                MotionEvent downEvent = MotionEvent.obtain(ev.getDownTime(), ev.getEventTime(), ev.getAction(), ev.getX(), mDownY, ev.getMetaState());
478
+                downEvent.setAction(MotionEvent.ACTION_DOWN);
479
+                handled = mList.dispatchTouchEvent(downEvent);
480
+                downEvent.recycle();
481
+                mHeaderOwnsTouch = false;
482
+            }
483
+        } else {
484
+            handled = mList.dispatchTouchEvent(ev);
485
+        }
486
+
487
+        return handled;
488
+    }
489
+
490
+    private class AdapterWrapperDataSetObserver extends DataSetObserver {
491
+
492
+        @Override
493
+        public void onChanged() {
494
+            clearHeader();
495
+        }
496
+
497
+        @Override
498
+        public void onInvalidated() {
499
+            clearHeader();
500
+        }
501
+
502
+    }
503
+
504
+    private class WrapperListScrollListener implements OnScrollListener {
505
+
506
+        @Override
507
+        public void onScroll(AbsListView view, int firstVisibleItem,
508
+                             int visibleItemCount, int totalItemCount) {
509
+            if (mOnScrollListenerDelegate != null) {
510
+                mOnScrollListenerDelegate.onScroll(view, firstVisibleItem,
511
+                        visibleItemCount, totalItemCount);
512
+            }
513
+            updateOrClearHeader(mList.getFixedFirstVisibleItem());
514
+        }
515
+
516
+        @Override
517
+        public void onScrollStateChanged(AbsListView view, int scrollState) {
518
+            if (mOnScrollListenerDelegate != null) {
519
+                mOnScrollListenerDelegate.onScrollStateChanged(view,
520
+                        scrollState);
521
+            }
522
+        }
523
+
524
+    }
525
+
526
+    private class WrapperViewListLifeCycleListener implements LifeCycleListener {
527
+
528
+        @Override
529
+        public void onDispatchDrawOccurred(Canvas canvas) {
530
+            // onScroll is not called often at all before froyo
531
+            // therefor we need to update the header here as well.
532
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
533
+                updateOrClearHeader(mList.getFixedFirstVisibleItem());
534
+            }
535
+            if (mHeader != null) {
536
+                if (mClippingToPadding) {
537
+                    canvas.save();
538
+                    canvas.clipRect(0, mPaddingTop, getRight(), getBottom());
539
+                    drawChild(canvas, mHeader, 0);
540
+                    canvas.restore();
541
+                } else {
542
+                    drawChild(canvas, mHeader, 0);
543
+                }
544
+            }
545
+        }
546
+
547
+    }
548
+
549
+    private class AdapterWrapperHeaderClickHandler implements
550
+            AdapterWrapper.OnHeaderClickListener {
551
+
552
+        @Override
553
+        public void onHeaderClick(View header, int itemPosition, long headerId) {
554
+            mOnHeaderClickListener.onHeaderClick(
555
+                    StickyListHeadersListView.this, header, itemPosition,
556
+                    headerId, false);
557
+        }
558
+
559
+    }
560
+
561
+    private boolean isStartOfSection(int position) {
562
+        return position == 0 || mAdapter.getHeaderId(position) != mAdapter.getHeaderId(position - 1);
563
+    }
564
+
565
+    public int getHeaderOverlap(int position) {
566
+        boolean isStartOfSection = isStartOfSection(Math.max(0, position - getHeaderViewsCount()));
567
+        if (!isStartOfSection) {
568
+            View header = mAdapter.getHeaderView(position, null, mList);
569
+            if (header == null) {
570
+                throw new NullPointerException("header may not be null");
571
+            }
572
+            ensureHeaderHasCorrectLayoutParams(header);
573
+            measureHeader(header);
574
+            return header.getMeasuredHeight();
575
+        }
576
+        return 0;
577
+    }
578
+
579
+    private int stickyHeaderTop() {
580
+        return mStickyHeaderTopOffset + (mClippingToPadding ? mPaddingTop : 0);
581
+    }
582
+
583
+    /* ---------- StickyListHeaders specific API ---------- */
584
+
585
+    public void setAreHeadersSticky(boolean areHeadersSticky) {
586
+        mAreHeadersSticky = areHeadersSticky;
587
+        if (!areHeadersSticky) {
588
+            clearHeader();
589
+        } else {
590
+            updateOrClearHeader(mList.getFixedFirstVisibleItem());
591
+        }
592
+        // invalidating the list will trigger dispatchDraw()
593
+        mList.invalidate();
594
+    }
595
+
596
+    public boolean areHeadersSticky() {
597
+        return mAreHeadersSticky;
598
+    }
599
+
600
+    /**
601
+     * Use areHeadersSticky() method instead
602
+     */
603
+    @Deprecated
604
+    public boolean getAreHeadersSticky() {
605
+        return areHeadersSticky();
606
+    }
607
+
608
+    /**
609
+     *
610
+     * @param stickyHeaderTopOffset
611
+     *          The offset of the sticky header fom the top of the view
612
+     */
613
+    public void setStickyHeaderTopOffset(int stickyHeaderTopOffset) {
614
+        mStickyHeaderTopOffset = stickyHeaderTopOffset;
615
+        updateOrClearHeader(mList.getFixedFirstVisibleItem());
616
+    }
617
+
618
+    public int getStickyHeaderTopOffset() {
619
+        return mStickyHeaderTopOffset;
620
+    }
621
+
622
+    public void setDrawingListUnderStickyHeader(
623
+            boolean drawingListUnderStickyHeader) {
624
+        mIsDrawingListUnderStickyHeader = drawingListUnderStickyHeader;
625
+        // reset the top clipping length
626
+        mList.setTopClippingLength(0);
627
+    }
628
+
629
+    public boolean isDrawingListUnderStickyHeader() {
630
+        return mIsDrawingListUnderStickyHeader;
631
+    }
632
+
633
+    public void setOnHeaderClickListener(OnHeaderClickListener listener) {
634
+        mOnHeaderClickListener = listener;
635
+        if (mAdapter != null) {
636
+            if (mOnHeaderClickListener != null) {
637
+                mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler());
638
+
639
+                if (mHeader != null) {
640
+                    mHeader.setOnClickListener(new OnClickListener() {
641
+                        @Override
642
+                        public void onClick(View v) {
643
+                            mOnHeaderClickListener.onHeaderClick(
644
+                                    StickyListHeadersListView.this, mHeader,
645
+                                    mHeaderPosition, mHeaderId, true);
646
+                        }
647
+                    });
648
+                }
649
+            } else {
650
+                mAdapter.setOnHeaderClickListener(null);
651
+            }
652
+        }
653
+    }
654
+
655
+    public void setOnStickyHeaderOffsetChangedListener(OnStickyHeaderOffsetChangedListener listener) {
656
+        mOnStickyHeaderOffsetChangedListener = listener;
657
+    }
658
+
659
+    public void setOnStickyHeaderChangedListener(OnStickyHeaderChangedListener listener) {
660
+        mOnStickyHeaderChangedListener = listener;
661
+    }
662
+
663
+    public View getListChildAt(int index) {
664
+        return mList.getChildAt(index);
665
+    }
666
+
667
+    public int getListChildCount() {
668
+        return mList.getChildCount();
669
+    }
670
+
671
+    /**
672
+     * Use the method with extreme caution!! Changing any values on the
673
+     * underlying ListView might break everything.
674
+     *
675
+     * @return the ListView backing this view.
676
+     */
677
+    public ListView getWrappedList() {
678
+        return mList;
679
+    }
680
+
681
+    private boolean requireSdkVersion(int versionCode) {
682
+        if (Build.VERSION.SDK_INT < versionCode) {
683
+            Log.e("StickyListHeaders", "Api lvl must be at least "+versionCode+" to call this method");
684
+            return false;
685
+        }
686
+        return true;
687
+    }
688
+
689
+	/* ---------- ListView delegate methods ---------- */
690
+
691
+    public void setAdapter(StickyListHeadersAdapter adapter) {
692
+        if (adapter == null) {
693
+            if (mAdapter instanceof SectionIndexerAdapterWrapper) {
694
+                ((SectionIndexerAdapterWrapper) mAdapter).mSectionIndexerDelegate = null;
695
+            }
696
+            if (mAdapter != null) {
697
+                mAdapter.mDelegate = null;
698
+            }
699
+            mList.setAdapter(null);
700
+            clearHeader();
701
+            return;
702
+        }
703
+        if (mAdapter != null) {
704
+            mAdapter.unregisterDataSetObserver(mDataSetObserver);
705
+        }
706
+
707
+        if (adapter instanceof SectionIndexer) {
708
+            mAdapter = new SectionIndexerAdapterWrapper(getContext(), adapter);
709
+        } else {
710
+            mAdapter = new AdapterWrapper(getContext(), adapter);
711
+        }
712
+        mDataSetObserver = new AdapterWrapperDataSetObserver();
713
+        mAdapter.registerDataSetObserver(mDataSetObserver);
714
+
715
+        if (mOnHeaderClickListener != null) {
716
+            mAdapter.setOnHeaderClickListener(new AdapterWrapperHeaderClickHandler());
717
+        } else {
718
+            mAdapter.setOnHeaderClickListener(null);
719
+        }
720
+
721
+        mAdapter.setDivider(mDivider, mDividerHeight);
722
+
723
+        mList.setAdapter(mAdapter);
724
+        clearHeader();
725
+    }
726
+
727
+    public StickyListHeadersAdapter getAdapter() {
728
+        return mAdapter == null ? null : mAdapter.mDelegate;
729
+    }
730
+
731
+    public void setDivider(Drawable divider) {
732
+        mDivider = divider;
733
+        if (mAdapter != null) {
734
+            mAdapter.setDivider(mDivider, mDividerHeight);
735
+        }
736
+    }
737
+
738
+    public void setDividerHeight(int dividerHeight) {
739
+        mDividerHeight = dividerHeight;
740
+        if (mAdapter != null) {
741
+            mAdapter.setDivider(mDivider, mDividerHeight);
742
+        }
743
+    }
744
+
745
+    public Drawable getDivider() {
746
+        return mDivider;
747
+    }
748
+
749
+    public int getDividerHeight() {
750
+        return mDividerHeight;
751
+    }
752
+
753
+    public void setOnScrollListener(OnScrollListener onScrollListener) {
754
+        mOnScrollListenerDelegate = onScrollListener;
755
+    }
756
+
757
+    @Override
758
+    public void setOnTouchListener(final OnTouchListener l) {
759
+        if (l != null) {
760
+            mList.setOnTouchListener(new OnTouchListener() {
761
+                @Override
762
+                public boolean onTouch(View v, MotionEvent event) {
763
+                    return l.onTouch(StickyListHeadersListView.this, event);
764
+                }
765
+            });
766
+        } else {
767
+            mList.setOnTouchListener(null);
768
+        }
769
+    }
770
+
771
+    public void setOnItemClickListener(OnItemClickListener listener) {
772
+        mList.setOnItemClickListener(listener);
773
+    }
774
+
775
+    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
776
+        mList.setOnItemLongClickListener(listener);
777
+    }
778
+
779
+    public void addHeaderView(View v, Object data, boolean isSelectable) {
780
+        mList.addHeaderView(v, data, isSelectable);
781
+    }
782
+
783
+    public void addHeaderView(View v) {
784
+        mList.addHeaderView(v);
785
+    }
786
+
787
+    public void removeHeaderView(View v) {
788
+        mList.removeHeaderView(v);
789
+    }
790
+
791
+    public int getHeaderViewsCount() {
792
+        return mList.getHeaderViewsCount();
793
+    }
794
+    
795
+    public void addFooterView(View v, Object data, boolean isSelectable) {
796
+        mList.addFooterView(v, data, isSelectable);
797
+    }
798
+
799
+    public void addFooterView(View v) {
800
+        mList.addFooterView(v);
801
+    }
802
+
803
+    public void removeFooterView(View v) {
804
+        mList.removeFooterView(v);
805
+    }
806
+
807
+    public int getFooterViewsCount() {
808
+        return mList.getFooterViewsCount();
809
+    }
810
+
811
+    public void setEmptyView(View v) {
812
+        mList.setEmptyView(v);
813
+    }
814
+
815
+    public View getEmptyView() {
816
+        return mList.getEmptyView();
817
+    }
818
+
819
+    @Override
820
+    public boolean isVerticalScrollBarEnabled() {
821
+        return mList.isVerticalScrollBarEnabled();
822
+    }
823
+
824
+    @Override
825
+    public boolean isHorizontalScrollBarEnabled() {
826
+        return mList.isHorizontalScrollBarEnabled();
827
+    }
828
+
829
+    @Override
830
+    public void setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled) {
831
+        mList.setVerticalScrollBarEnabled(verticalScrollBarEnabled);
832
+    }
833
+
834
+    @Override
835
+    public void setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled) {
836
+        mList.setHorizontalScrollBarEnabled(horizontalScrollBarEnabled);
837
+    }
838
+
839
+    @Override
840
+    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
841
+    public int getOverScrollMode() {
842
+        if (requireSdkVersion(Build.VERSION_CODES.GINGERBREAD)) {
843
+            return mList.getOverScrollMode();
844
+        }
845
+        return 0;
846
+    }
847
+
848
+    @Override
849
+    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
850
+    public void setOverScrollMode(int mode) {
851
+        if (requireSdkVersion(Build.VERSION_CODES.GINGERBREAD)) {
852
+            if (mList != null) {
853
+                mList.setOverScrollMode(mode);
854
+            }
855
+        }
856
+    }
857
+
858
+    @TargetApi(Build.VERSION_CODES.FROYO)
859
+    public void smoothScrollBy(int distance, int duration) {
860
+        if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
861
+            mList.smoothScrollBy(distance, duration);
862
+        }
863
+    }
864
+
865
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
866
+    public void smoothScrollByOffset(int offset) {
867
+        if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
868
+            mList.smoothScrollByOffset(offset);
869
+        }
870
+    }
871
+
872
+    @SuppressLint("NewApi")
873
+    @TargetApi(Build.VERSION_CODES.FROYO)
874
+    public void smoothScrollToPosition(int position) {
875
+        if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
876
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
877
+                mList.smoothScrollToPosition(position);
878
+            } else {
879
+                int offset = mAdapter == null ? 0 : getHeaderOverlap(position);
880
+                offset -= mClippingToPadding ? 0 : mPaddingTop;
881
+                mList.smoothScrollToPositionFromTop(position, offset);
882
+            }
883
+        }
884
+    }
885
+
886
+    @TargetApi(Build.VERSION_CODES.FROYO)
887
+    public void smoothScrollToPosition(int position, int boundPosition) {
888
+        if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
889
+            mList.smoothScrollToPosition(position, boundPosition);
890
+        }
891
+    }
892
+
893
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
894
+    public void smoothScrollToPositionFromTop(int position, int offset) {
895
+        if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
896
+            offset += mAdapter == null ? 0 : getHeaderOverlap(position);
897
+            offset -= mClippingToPadding ? 0 : mPaddingTop;
898
+            mList.smoothScrollToPositionFromTop(position, offset);
899
+        }
900
+    }
901
+
902
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
903
+    public void smoothScrollToPositionFromTop(int position, int offset,
904
+                                              int duration) {
905
+        if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
906
+            offset += mAdapter == null ? 0 : getHeaderOverlap(position);
907
+            offset -= mClippingToPadding ? 0 : mPaddingTop;
908
+            mList.smoothScrollToPositionFromTop(position, offset, duration);
909
+        }
910
+    }
911
+
912
+    public void setSelection(int position) {
913
+        setSelectionFromTop(position, 0);
914
+    }
915
+
916
+    public void setSelectionAfterHeaderView() {
917
+        mList.setSelectionAfterHeaderView();
918
+    }
919
+
920
+    public void setSelectionFromTop(int position, int y) {
921
+        y += mAdapter == null ? 0 : getHeaderOverlap(position);
922
+        y -= mClippingToPadding ? 0 : mPaddingTop;
923
+        mList.setSelectionFromTop(position, y);
924
+    }
925
+
926
+    public void setSelector(Drawable sel) {
927
+        mList.setSelector(sel);
928
+    }
929
+
930
+    public void setSelector(int resID) {
931
+        mList.setSelector(resID);
932
+    }
933
+
934
+    public int getFirstVisiblePosition() {
935
+        return mList.getFirstVisiblePosition();
936
+    }
937
+
938
+    public int getLastVisiblePosition() {
939
+        return mList.getLastVisiblePosition();
940
+    }
941
+
942
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
943
+    public void setChoiceMode(int choiceMode) {
944
+        mList.setChoiceMode(choiceMode);
945
+    }
946
+
947
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
948
+    public void setItemChecked(int position, boolean value) {
949
+        mList.setItemChecked(position, value);
950
+    }
951
+
952
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
953
+    public int getCheckedItemCount() {
954
+        if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
955
+            return mList.getCheckedItemCount();
956
+        }
957
+        return 0;
958
+    }
959
+
960
+    @TargetApi(Build.VERSION_CODES.FROYO)
961
+    public long[] getCheckedItemIds() {
962
+        if (requireSdkVersion(Build.VERSION_CODES.FROYO)) {
963
+            return mList.getCheckedItemIds();
964
+        }
965
+        return null;
966
+    }
967
+
968
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
969
+    public int getCheckedItemPosition() {
970
+        return mList.getCheckedItemPosition();
971
+    }
972
+
973
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
974
+    public SparseBooleanArray getCheckedItemPositions() {
975
+        return mList.getCheckedItemPositions();
976
+    }
977
+
978
+    public int getCount() {
979
+        return mList.getCount();
980
+    }
981
+
982
+    public Object getItemAtPosition(int position) {
983
+        return mList.getItemAtPosition(position);
984
+    }
985
+
986
+    public long getItemIdAtPosition(int position) {
987
+        return mList.getItemIdAtPosition(position);
988
+    }
989
+
990
+    @Override
991
+    public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
992
+        mList.setOnCreateContextMenuListener(l);
993
+    }
994
+
995
+    @Override
996
+    public boolean showContextMenu() {
997
+        return mList.showContextMenu();
998
+    }
999
+
1000
+    public void invalidateViews() {
1001
+        mList.invalidateViews();
1002
+    }
1003
+
1004
+    @Override
1005
+    public void setClipToPadding(boolean clipToPadding) {
1006
+        if (mList != null) {
1007
+            mList.setClipToPadding(clipToPadding);
1008
+        }
1009
+        mClippingToPadding = clipToPadding;
1010
+    }
1011
+
1012
+    @Override
1013
+    public void setPadding(int left, int top, int right, int bottom) {
1014
+        mPaddingLeft = left;
1015
+        mPaddingTop = top;
1016
+        mPaddingRight = right;
1017
+        mPaddingBottom = bottom;
1018
+
1019
+        if (mList != null) {
1020
+            mList.setPadding(left, top, right, bottom);
1021
+        }
1022
+        super.setPadding(0, 0, 0, 0);
1023
+        requestLayout();
1024
+    }
1025
+
1026
+    /*
1027
+     * Overrides an @hide method in View
1028
+     */
1029
+    protected void recomputePadding() {
1030
+        setPadding(mPaddingLeft, mPaddingTop, mPaddingRight, mPaddingBottom);
1031
+    }
1032
+
1033
+    @Override
1034
+    public int getPaddingLeft() {
1035
+        return mPaddingLeft;
1036
+    }
1037
+
1038
+    @Override
1039
+    public int getPaddingTop() {
1040
+        return mPaddingTop;
1041
+    }
1042
+
1043
+    @Override
1044
+    public int getPaddingRight() {
1045
+        return mPaddingRight;
1046
+    }
1047
+
1048
+    @Override
1049
+    public int getPaddingBottom() {
1050
+        return mPaddingBottom;
1051
+    }
1052
+
1053
+    public void setFastScrollEnabled(boolean fastScrollEnabled) {
1054
+        mList.setFastScrollEnabled(fastScrollEnabled);
1055
+    }
1056
+
1057
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
1058
+    public void setFastScrollAlwaysVisible(boolean alwaysVisible) {
1059
+        if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
1060
+            mList.setFastScrollAlwaysVisible(alwaysVisible);
1061
+        }
1062
+    }
1063
+
1064
+    /**
1065
+     * @return true if the fast scroller will always show. False on pre-Honeycomb devices.
1066
+     * @see AbsListView#isFastScrollAlwaysVisible()
1067
+     */
1068
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
1069
+    public boolean isFastScrollAlwaysVisible() {
1070
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
1071
+            return false;
1072
+        }
1073
+        return mList.isFastScrollAlwaysVisible();
1074
+    }
1075
+
1076
+    public void setScrollBarStyle(int style) {
1077
+        mList.setScrollBarStyle(style);
1078
+    }
1079
+
1080
+    public int getScrollBarStyle() {
1081
+        return mList.getScrollBarStyle();
1082
+    }
1083
+
1084
+    public int getPositionForView(View view) {
1085
+        return mList.getPositionForView(view);
1086
+    }
1087
+
1088
+    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
1089
+    public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
1090
+        if (requireSdkVersion(Build.VERSION_CODES.HONEYCOMB)) {
1091
+            mList.setMultiChoiceModeListener(listener);
1092
+        }
1093
+    }
1094
+
1095
+    @Override
1096
+    public Parcelable onSaveInstanceState() {
1097
+        Parcelable superState = super.onSaveInstanceState();
1098
+        if (superState != BaseSavedState.EMPTY_STATE) {
1099
+          throw new IllegalStateException("Handling non empty state of parent class is not implemented");
1100
+        }
1101
+        return mList.onSaveInstanceState();
1102
+    }
1103
+
1104
+    @Override
1105
+    public void onRestoreInstanceState(Parcelable state) {
1106
+        super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE);
1107
+        mList.onRestoreInstanceState(state);
1108
+    }
1109
+
1110
+    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1111
+    @Override
1112
+    public boolean canScrollVertically(int direction) {
1113
+        return mList.canScrollVertically(direction);
1114
+    }
1115
+
1116
+    public void setTranscriptMode (int mode) {
1117
+        mList.setTranscriptMode(mode);
1118
+    }
1119
+
1120
+    public void setBlockLayoutChildren(boolean blockLayoutChildren) {
1121
+        mList.setBlockLayoutChildren(blockLayoutChildren);
1122
+    }
1123
+    
1124
+    public void setStackFromBottom(boolean stackFromBottom) {
1125
+    	mList.setStackFromBottom(stackFromBottom);
1126
+    }
1127
+
1128
+    public boolean isStackFromBottom() {
1129
+    	return mList.isStackFromBottom();
1130
+    }
1131
+}

+ 156 - 0
views/src/main/java/com/android/views/stickylistheaders/WrapperView.java

@@ -0,0 +1,156 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.content.Context;
4
+import android.graphics.Canvas;
5
+import android.graphics.drawable.Drawable;
6
+import android.os.Build;
7
+import android.view.View;
8
+import android.view.ViewGroup;
9
+import android.view.ViewParent;
10
+
11
+/**
12
+ * 
13
+ * the view that wrapps a divider header and a normal list item. The listview sees this as 1 item
14
+ * 
15
+ * @author Emil Sjölander
16
+ */
17
+public class WrapperView extends ViewGroup {
18
+
19
+	View mItem;
20
+	Drawable mDivider;
21
+	int mDividerHeight;
22
+	View mHeader;
23
+	int mItemTop;
24
+
25
+	WrapperView(Context c) {
26
+		super(c);
27
+	}
28
+
29
+	public boolean hasHeader() {
30
+		return mHeader != null;
31
+	}
32
+	
33
+	public View getItem() {
34
+		return mItem;
35
+	}
36
+	
37
+	public View getHeader() {
38
+		return mHeader;
39
+	}
40
+
41
+	void update(View item, View header, Drawable divider, int dividerHeight) {
42
+		
43
+		//every wrapperview must have a list item
44
+		if (item == null) {
45
+			throw new NullPointerException("List view item must not be null.");
46
+		}
47
+
48
+		//only remove the current item if it is not the same as the new item. this can happen if wrapping a recycled view
49
+		if (this.mItem != item) {
50
+			removeView(this.mItem);
51
+			this.mItem = item;
52
+			final ViewParent parent = item.getParent();
53
+			if(parent != null && parent != this) {
54
+				if(parent instanceof ViewGroup) {
55
+					((ViewGroup) parent).removeView(item);
56
+				}
57
+			}
58
+			addView(item);
59
+		}
60
+
61
+		//same logik as above but for the header
62
+		if (this.mHeader != header) {
63
+			if (this.mHeader != null) {
64
+				removeView(this.mHeader);
65
+			}
66
+			this.mHeader = header;
67
+			if (header != null) {
68
+				addView(header);
69
+			}
70
+		}
71
+
72
+		if (this.mDivider != divider) {
73
+			this.mDivider = divider;
74
+			this.mDividerHeight = dividerHeight;
75
+			invalidate();
76
+		}
77
+	}
78
+
79
+	@Override
80
+	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
81
+		int measuredWidth = MeasureSpec.getSize(widthMeasureSpec);
82
+		int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(measuredWidth,
83
+				MeasureSpec.EXACTLY);
84
+		int measuredHeight = 0;
85
+		
86
+		//measure header or divider. when there is a header visible it acts as the divider
87
+		if (mHeader != null) {
88
+			LayoutParams params = mHeader.getLayoutParams();
89
+			if (params != null && params.height > 0) {
90
+				mHeader.measure(childWidthMeasureSpec,
91
+						MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY));
92
+			} else {
93
+				mHeader.measure(childWidthMeasureSpec,
94
+						MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
95
+			}
96
+			measuredHeight += mHeader.getMeasuredHeight();
97
+		} else if (mDivider != null&&mItem.getVisibility()!=View.GONE) {
98
+			measuredHeight += mDividerHeight;
99
+		}
100
+
101
+		//measure item
102
+		LayoutParams params = mItem.getLayoutParams();
103
+        //enable hiding listview item,ex. toggle off items in group
104
+		if(mItem.getVisibility()==View.GONE){
105
+            mItem.measure(childWidthMeasureSpec,
106
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY));
107
+        }else if (params != null && params.height >= 0) {
108
+			mItem.measure(childWidthMeasureSpec,
109
+					MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY));
110
+            measuredHeight += mItem.getMeasuredHeight();
111
+		} else {
112
+			mItem.measure(childWidthMeasureSpec,
113
+					MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
114
+            measuredHeight += mItem.getMeasuredHeight();
115
+		}
116
+
117
+
118
+		setMeasuredDimension(measuredWidth, measuredHeight);
119
+	}
120
+
121
+	@Override
122
+	protected void onLayout(boolean changed, int l, int t, int r, int b) {
123
+
124
+		l = 0;
125
+		t = 0;
126
+		r = getWidth();
127
+		b = getHeight();
128
+
129
+		if (mHeader != null) {
130
+			int headerHeight = mHeader.getMeasuredHeight();
131
+			mHeader.layout(l, t, r, headerHeight);
132
+			mItemTop = headerHeight;
133
+			mItem.layout(l, headerHeight, r, b);
134
+		} else if (mDivider != null) {
135
+			mDivider.setBounds(l, t, r, mDividerHeight);
136
+			mItemTop = mDividerHeight;
137
+			mItem.layout(l, mDividerHeight, r, b);
138
+		} else {
139
+			mItemTop = t;
140
+			mItem.layout(l, t, r, b);
141
+		}
142
+	}
143
+
144
+	@Override
145
+	protected void dispatchDraw(Canvas canvas) {
146
+		super.dispatchDraw(canvas);
147
+		if (mHeader == null && mDivider != null&&mItem.getVisibility()!=View.GONE) {
148
+			// Drawable.setBounds() does not seem to work pre-honeycomb. So have
149
+			// to do this instead
150
+			if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
151
+				canvas.clipRect(0, 0, getWidth(), mDividerHeight);
152
+			}
153
+			mDivider.draw(canvas);
154
+		}
155
+	}
156
+}

+ 196 - 0
views/src/main/java/com/android/views/stickylistheaders/WrapperViewList.java

@@ -0,0 +1,196 @@
1
+package com.android.views.stickylistheaders;
2
+
3
+import android.content.Context;
4
+import android.graphics.Canvas;
5
+import android.graphics.Rect;
6
+import android.os.Build;
7
+import android.view.View;
8
+import android.widget.AbsListView;
9
+import android.widget.ListView;
10
+
11
+import java.lang.reflect.Field;
12
+import java.util.ArrayList;
13
+import java.util.List;
14
+
15
+class WrapperViewList extends ListView {
16
+
17
+	interface LifeCycleListener {
18
+		void onDispatchDrawOccurred(Canvas canvas);
19
+	}
20
+
21
+	private LifeCycleListener mLifeCycleListener;
22
+	private List<View> mFooterViews;
23
+	private int mTopClippingLength;
24
+	private Rect mSelectorRect = new Rect();// for if reflection fails
25
+	private Field mSelectorPositionField;
26
+	private boolean mClippingToPadding = true;
27
+    private boolean mBlockLayoutChildren = false;
28
+
29
+	public WrapperViewList(Context context) {
30
+		super(context);
31
+
32
+		// Use reflection to be able to change the size/position of the list
33
+		// selector so it does not come under/over the header
34
+		try {
35
+			Field selectorRectField = AbsListView.class.getDeclaredField("mSelectorRect");
36
+			selectorRectField.setAccessible(true);
37
+			mSelectorRect = (Rect) selectorRectField.get(this);
38
+
39
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
40
+				mSelectorPositionField = AbsListView.class.getDeclaredField("mSelectorPosition");
41
+				mSelectorPositionField.setAccessible(true);
42
+			}
43
+		} catch (NoSuchFieldException e) {
44
+			e.printStackTrace();
45
+		} catch (IllegalArgumentException e) {
46
+			e.printStackTrace();
47
+		} catch (IllegalAccessException e) {
48
+			e.printStackTrace();
49
+		}
50
+	}
51
+
52
+	@Override
53
+	public boolean performItemClick(View view, int position, long id) {
54
+		if (view instanceof WrapperView) {
55
+			view = ((WrapperView) view).mItem;
56
+		}
57
+		return super.performItemClick(view, position, id);
58
+	}
59
+
60
+	private void positionSelectorRect() {
61
+		if (!mSelectorRect.isEmpty()) {
62
+			int selectorPosition = getSelectorPosition();
63
+			if (selectorPosition >= 0) {
64
+				int firstVisibleItem = getFixedFirstVisibleItem();
65
+				View v = getChildAt(selectorPosition - firstVisibleItem);
66
+				if (v instanceof WrapperView) {
67
+					WrapperView wrapper = ((WrapperView) v);
68
+					mSelectorRect.top = wrapper.getTop() + wrapper.mItemTop;
69
+				}
70
+			}
71
+		}
72
+	}
73
+
74
+	private int getSelectorPosition() {
75
+		if (mSelectorPositionField == null) { // not all supported andorid
76
+			// version have this variable
77
+			for (int i = 0; i < getChildCount(); i++) {
78
+				if (getChildAt(i).getBottom() == mSelectorRect.bottom) {
79
+					return i + getFixedFirstVisibleItem();
80
+				}
81
+			}
82
+		} else {
83
+			try {
84
+				return mSelectorPositionField.getInt(this);
85
+			} catch (IllegalArgumentException e) {
86
+				e.printStackTrace();
87
+			} catch (IllegalAccessException e) {
88
+				e.printStackTrace();
89
+			}
90
+		}
91
+		return -1;
92
+	}
93
+
94
+	@Override
95
+	protected void dispatchDraw(Canvas canvas) {
96
+		positionSelectorRect();
97
+		if (mTopClippingLength != 0) {
98
+			canvas.save();
99
+			Rect clipping = canvas.getClipBounds();
100
+			clipping.top = mTopClippingLength;
101
+			canvas.clipRect(clipping);
102
+			super.dispatchDraw(canvas);
103
+			canvas.restore();
104
+		} else {
105
+			super.dispatchDraw(canvas);
106
+		}
107
+		mLifeCycleListener.onDispatchDrawOccurred(canvas);
108
+	}
109
+
110
+	void setLifeCycleListener(LifeCycleListener lifeCycleListener) {
111
+		mLifeCycleListener = lifeCycleListener;
112
+	}
113
+
114
+	@Override
115
+	public void addFooterView(View v) {
116
+		super.addFooterView(v);
117
+		addInternalFooterView(v);
118
+	}
119
+
120
+	@Override
121
+	public void addFooterView(View v, Object data, boolean isSelectable) {
122
+		super.addFooterView(v, data, isSelectable);
123
+		addInternalFooterView(v);
124
+	}
125
+
126
+	private void addInternalFooterView(View v) {
127
+		if (mFooterViews == null) {
128
+			mFooterViews = new ArrayList<View>();
129
+		}
130
+		mFooterViews.add(v);
131
+	}
132
+
133
+	@Override
134
+	public boolean removeFooterView(View v) {
135
+		if (super.removeFooterView(v)) {
136
+			mFooterViews.remove(v);
137
+			return true;
138
+		}
139
+		return false;
140
+	}
141
+
142
+	boolean containsFooterView(View v) {
143
+		if (mFooterViews == null) {
144
+			return false;
145
+		}
146
+		return mFooterViews.contains(v);
147
+	}
148
+
149
+	void setTopClippingLength(int topClipping) {
150
+		mTopClippingLength = topClipping;
151
+	}
152
+
153
+	int getFixedFirstVisibleItem() {
154
+		int firstVisibleItem = getFirstVisiblePosition();
155
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
156
+			return firstVisibleItem;
157
+		}
158
+
159
+		// first getFirstVisiblePosition() reports items
160
+		// outside the view sometimes on old versions of android
161
+		for (int i = 0; i < getChildCount(); i++) {
162
+			if (getChildAt(i).getBottom() >= 0) {
163
+				firstVisibleItem += i;
164
+				break;
165
+			}
166
+		}
167
+
168
+		// work around to fix bug with firstVisibleItem being to high
169
+		// because list view does not take clipToPadding=false into account
170
+		// on old versions of android
171
+		if (!mClippingToPadding && getPaddingTop() > 0 && firstVisibleItem > 0) {
172
+			if (getChildAt(0).getTop() > 0) {
173
+				firstVisibleItem -= 1;
174
+			}
175
+		}
176
+
177
+		return firstVisibleItem;
178
+	}
179
+
180
+	@Override
181
+	public void setClipToPadding(boolean clipToPadding) {
182
+		mClippingToPadding = clipToPadding;
183
+		super.setClipToPadding(clipToPadding);
184
+	}
185
+
186
+    public void setBlockLayoutChildren(boolean block) {
187
+        mBlockLayoutChildren = block;
188
+    }
189
+
190
+    @Override
191
+    protected void layoutChildren() {
192
+        if (!mBlockLayoutChildren) {
193
+            super.layoutChildren();
194
+        }
195
+    }
196
+}

+ 34 - 0
views/src/main/res/values/attrs.xml

@@ -87,4 +87,38 @@
87 87
     </declare-styleable>
88 88
 
89 89
     <attr name="SwipeBackLayoutStyle" format="reference"/>
90
+
91
+    <declare-styleable name="StickyListHeadersListView">
92
+        <attr name="stickyListHeadersListViewStyle" format="reference"/>
93
+
94
+        <!-- View attributes -->
95
+        <attr name="android:clipToPadding" />
96
+        <attr name="android:scrollbars" />
97
+        <attr name="android:overScrollMode" />
98
+        <attr name="android:padding" />
99
+        <attr name="android:paddingLeft" />
100
+        <attr name="android:paddingTop" />
101
+        <attr name="android:paddingRight" />
102
+        <attr name="android:paddingBottom" />
103
+
104
+        <!-- ListView attributes -->
105
+        <attr name="android:fadingEdgeLength" />
106
+        <attr name="android:requiresFadingEdge" />
107
+        <attr name="android:cacheColorHint" />
108
+        <attr name="android:choiceMode" />
109
+        <attr name="android:drawSelectorOnTop" />
110
+        <attr name="android:fastScrollEnabled" />
111
+        <attr name="android:fastScrollAlwaysVisible" />
112
+        <attr name="android:listSelector" />
113
+        <attr name="android:scrollingCache" />
114
+        <attr name="android:scrollbarStyle" />
115
+        <attr name="android:divider" />
116
+        <attr name="android:dividerHeight" />
117
+        <attr name="android:transcriptMode" />
118
+        <attr name="android:stackFromBottom" />
119
+
120
+        <!-- StickyListHeaders attributes -->
121
+        <attr name="hasStickyHeaders" format="boolean" />
122
+        <attr name="isDrawingListUnderStickyHeader" format="boolean" />
123
+    </declare-styleable>
90 124
 </resources>